import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.*;

/**
 * A Client for communication between different machines
 * @author Søren Frederiksen and Lars Christian Ersly
 */
public class MultiChat {
    /** The port that all MultiChat clients will listen for new connections on */
    public static final int defaultPort = 40308;
    /** The port used for sending old messages to display in history*/
    public static final int historyPort = defaultPort + 10000;
    /** The address of this client */
    private InetSocketAddress thisClientAddress;

    /** The queue for sending and recieving messages */
    private MulticastQueueFifoOnly<String> queueFifoOnly;
    /** The list containing all previously sent messages */
    private ArrayList<MulticastMessage> messageHistory;
    /** List of all peers connected to the network */
    private Set<InetAddress> peers = new HashSet<InetAddress>();

    /** The thread responsible for recieving messages from other clients */
    private RecievingThread recievingThread;
    /** The thread responsible for reading from std.in and sending it to the other clients */
    private SendingThread sendingThread;

    /** if "exit" has been found */
    private boolean shutdown = false;
    /** Should debug messages be enabled? */
    private boolean debugMessages = false;

    public static void main(String[] args){
        //Start a new group
        if(args.length == 0) {
            new MultiChat();
        }
        //join an existing group
        else if(args.length == 1){
            new MultiChat(new InetSocketAddress( args[0], defaultPort ));
        }
        //print usage
        else{
            System.out.println("no arguments creates a client waiting for connections");
            System.out.println("one argument creates a client and tries to connect to the group on the address");
        }
    }

    /**
     * Constructs a new FIFO MultiChat client listening for connections on the default port.
     */
    public MultiChat() {
        initialize();

        try {
            queueFifoOnly.createGroup(defaultPort, MulticastQueue.DeliveryGuarantee.FIFO);
        } catch (IOException e) {
            System.err.println("Error opening the port");
            e.printStackTrace(); System.exit(-1);
        }
        smallSleep();

        run();
    }

    /**
     * Constructs a FIFO MultiChat client, and joins the group at the specified address
     * Also handles recieving old message history
     * @param address the server address
     */
    public MultiChat(InetSocketAddress address){
        initialize();

        //Open a channel for recieving the history from one of the other clients.
        PointToPointQueueReceiverEnd<MulticastMessage> p2pHistoryReciever =
                new PointToPointQueueReceiverEndNonRobust<MulticastMessage>();
        try{
            p2pHistoryReciever.listenOnPort(historyPort);
        } catch (IOException e) {
            System.err.println("Error opening the port used for transferring history: " + historyPort);
            e.printStackTrace(); System.exit(-1);
        }

        //join the group
        try {
            queueFifoOnly.joinGroup(defaultPort, address, MulticastQueue.DeliveryGuarantee.FIFO);
        } catch (IOException e) {
            System.err.println("Error joining group on the address: " + address);
            e.printStackTrace(); System.exit(-1);
        }
        smallSleep();

        //Getting the older messages, and printing them
        System.out.println("[HISTORY BEGIN]");
        MulticastMessage msg;
        while((msg = p2pHistoryReciever.get()) != null){
            if(msg instanceof MulticastMessageEoH){
                //an message of this type signals "End of History"
                break;
            }
            messageHistory.add(msg);
            handleMessage(msg, true);
        }
        System.out.println("[HISTORY END]\n");
        p2pHistoryReciever.shutdown();

        //start the client
        run();
    }

    /** Initializes some of the common stuff, and prints the welcome message */
    private void initialize(){
        //Get the address of this client
        String localHostAddress = null;
        try {
            localHostAddress = InetAddress.getLocalHost().getCanonicalHostName();
        } catch (UnknownHostException e) {
            System.out.println("Could not get local address");
            e.printStackTrace();
            System.exit(-1);
        }
        thisClientAddress = new InetSocketAddress(localHostAddress, defaultPort);

        queueFifoOnly = new MulticastQueueFifoOnly<String>();
        messageHistory = new ArrayList<MulticastMessage>();

        recievingThread = new RecievingThread();
        sendingThread = new SendingThread();

        System.out.println("Welcome to MultiChat");
        System.out.println("You are [" + thisClientAddress + "]");
        System.out.println("Type \"exit\" <ENTER> to end session\n");
    }

    /** Starts the threads */
    private void run(){
        sendingThread.start();
        smallSleep();
        recievingThread.start();
        smallSleep();

        log("Main thread: Waiting for other threads to finish");
        try{
            sendingThread.join();
            recievingThread.join();
        } catch (InterruptedException e) {
            System.err.println("Error waiting for threads to finish");
            e.printStackTrace();
        }
        log("Main thread: Threads are finished");

        System.out.println("Disconnected");
    }

    /**
     * Handles the proper recieving of messages
     * @param multicastMessage the message recieved
     * @param history true if this is an old message
     */
    private void handleMessage(MulticastMessage multicastMessage, boolean history){
        if(multicastMessage instanceof MulticastMessagePayload)
        {
            printMessage((MulticastMessagePayload<String>) multicastMessage); //some pretty printing of the message
        }
        else if(multicastMessage instanceof MulticastMessageJoin)
        {
            System.out.println(multicastMessage);

            //if we aren't currently going through old messages, send the history of messages to the new client
            if(!history) sendHistory( multicastMessage.getSender().getAddress() );

            //add it to the list of active peers
            peers.add(multicastMessage.getSender().getAddress());
        } 
        else if(multicastMessage instanceof MulticastMessageLeave)
        {
            peers.remove(multicastMessage.getSender().getAddress());
            System.out.println(multicastMessage);
        }
        else if(multicastMessage instanceof MulticastMessageEoH)
        {
            log("It should not be possible to recieve one of these here: " + multicastMessage);
        }
        else
        {
            log("Some strange message recieved: " + multicastMessage);
        }
    }

    /**
     * Sends the history to a newly joined client.
     * But only if this instance is the one with the lexicographic highest name
     * @param address The address of the newly joined client
     */
    private void sendHistory(InetAddress address) {
        //loop through all peers finding the peer specified by compare()
        InetAddress minAddress = null;
        for(InetAddress add : peers){
            minAddress = compare(minAddress , add);
        }

        //if our address is the same as the one found...
        if (thisClientAddress.getAddress().equals(minAddress)){
            log(thisClientAddress + " is sending the history which contains " + messageHistory.size() + " elements");

            //Create the socket address matching the sender's ip, and the history port
            InetSocketAddress senderHistory = new InetSocketAddress(
                    address,
                    historyPort
            );

            //open connection to the newly joined client
            PointToPointQueueSenderEnd<MulticastMessage> queue =
                    new PointToPointQueueSenderEndNonRobust<MulticastMessage>();
            queue.setReceiver(senderHistory);

            //send all messages recieved.
            for(MulticastMessage msg : messageHistory){
                queue.put(msg);
            }
            queue.put(new MulticastMessageEoH(thisClientAddress)); //finish by sending a special message
            queue.shutdown();
        }
    }

    /**
     * Compares two InetAddress's, returns one of them. Both cant be null
     * @param a1 address 1
     * @param a2 address 2
     * @return the InetAddress with the lexicographical highest address
     */
    private InetAddress compare(InetAddress a1, InetAddress a2){
        if(a2 == null) return a1;
        if(a1 == null) return a2;

        if(a1.toString().compareTo(a2.toString()) > 0){
            return a1;
        }
        return a2;
    }

    private void printMessage(MulticastMessagePayload<String> payload){
        System.out.println(payload.getSender() + " says " + payload.getPayload());
    }

    private void log(String logMsg){
        if(debugMessages) System.out.println(logMsg);
    }

    private void smallSleep(){
        try{
            Thread.sleep(100);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }

    /**
     * Handles both the reading from System.in and the forwarding to the other peers
     */
    private class SendingThread extends Thread{
        public void run(){
            log("Sending thread: starting");

            //reading from System.in
            BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in));

            while(!shutdown){
                try {
                    String stdInRead;
                    if((stdInRead = stdIn.readLine()) != null){

                        //check if valid exit command
                        if(stdInRead.equals("exit")){
                            log("Exit command recieved");
                            shutdown = true;
                            queueFifoOnly.leaveGroup();
                        }
                        else{ //else just send it to the other clients
                            queueFifoOnly.put(stdInRead);
                            sleep(100);
                        }
                    }
                } catch (Exception e) {
                    System.err.println("Something went wrong in the Sending thread");  e.printStackTrace();
                    System.exit(-1);
                }
            }
            log("Sending thread: finished");
        }
    }

    /**
     * Handles the recieving of messages from other clients
     */
    private class RecievingThread extends Thread {
        public void run() {
            log("Recieving thread: staring");

            while(!shutdown){
                //get the next message from the queue
                MulticastMessage multicastMessage = queueFifoOnly.get();
                try {
                    sleep(100);
                } catch (InterruptedException ignored) {}

                //send the message to get proper handling
                handleMessage(multicastMessage, false);

                //add it to the list of old messages
                messageHistory.add(multicastMessage);
            }

            //get() specifies that it will only return null when this peer officially has left the group
            log("Recieving thread: emptying queue");
            while(queueFifoOnly.get() != null){}
            
            log("Recieving thread: finished");
        }
    }
}
